20.3 错误

不知从何时起,panic就成了一个禁忌话题,诸多教程里都有“Don’t Panic!”这样的条例。这让我想起Python__del__的话题,两者颇为类似。其实,对于不可恢复性的错误用panic并无不妥,见仁见智吧。

从源码看,panic/recover的实现和defer息息相关,且过程算不上复杂。

runtime2.go

type _panic struct { argp unsafe.Pointer // pointer to arguments of deferred call run during panic arg interface{} // argument to panic link *_panic // link to earlier panic recovered bool // whether this panic is over aborted bool // the panic was aborted } type _defer struct { _panic *_panic } type g struct { _panic *_panic }

编译器将panic翻译成gopainc函数调用。它会将错误信息打包成_panic对象,并挂到G._panic链表的头部,然后遍历执行G._defer链表,检查是否recover。如被recover,则终止遍历执行,跳转到正常的deferreturn环节。否则执行整个调用堆栈的延迟函数后,显示异常信息,终止进程。

panic.go

func gopanic(e interface{}) { gp := getg() // 新建 _painc,挂到 G._panic 链表头部 var p _panic p.arg = e p.link = gp._panic gp._panic = (_panic)(noescape(unsafe.Pointer(&p))) // 遍历执行 G._defer(整个调用堆栈),直到某个 recover for { d := gp._defer if d == nil { break } // 如果 defer 已经执行,继续下一个 if d.started { if d._panic != nil { d._panic.aborted = true } d._panic = nil d.fn = nil gp._defer = d.link freedefer(d) continue } // 不移除 defer,便于 traceback 输出所有调用堆栈信息 d.started = true // 将 _panic 保存到 defer._panic d._panic = (_panic)(noescape((unsafe.Pointer)(&p))) // 执行 defer 函数 // p.argp 地址很重要,defer 里的 recover 以此来判断是否直接在 defer 内执行 // reflectcall 会修改 p.argp p.argp = unsafe.Pointer(getargp(0)) reflectcall(nil, unsafe.Pointer(d.fn), deferArgs(d), uint32(d.siz), uint32(d.siz)) p.argp = nil // 将已经执行的 defer 从 G._defer 链表移除 d._panic = nil d.fn = nil gp._defer = d.link pc := d.pc sp := unsafe.Pointer(d.sp) freedefer(d) // 如果该 defer 内执行了 recover,那么 recovered = true if p.recovered { // 移除当前 recovered panic gp._panic = p.link // 移除 aborted panic for gp._panic != nil && gp._panic.aborted { gp._panic = gp._panic.link } // recovery 会跳转会 defer.pc,也就是调用 deferproc 后 // 编译器会调用 deferproc 后插入比较指令,通过标志判断,跳转 // 到 deferreturn 执行剩余的 defer 函数 gp.sigcode0 = uintptr(sp) gp.sigcode1 = pc mcall(recovery) throw(“recovery failed”) // mcall should not return } // 如果没有 recovered,那么循环执行整个调用堆栈的延迟函数, // 要么被后续 recover,要么崩溃 } // 如果没有捕获,显示错误信息后终止(exit)进程 startpanic() printpanics(gp._panic) dopanic(0) // should not return *(*int)(nil) = 0 // not reached }

和panic相比,recover函数除返回最后一个错误信息外,主要是设置recovered标志。注意,它会通过参数堆栈地址确认是否在延迟函数内被直接调用。

panic.go

func gorecover(argp uintptr) interface{} { gp := getg() p := gp._panic if p != nil && !p.recovered && argp == uintptr(p.argp) { p.recovered = true return p.arg } return nil }